动画和过渡
Scripting 通过 Observable / useObservable、Animation、Transition、withAnimation 以及视图的 animation / transition 属性,基本对齐了 SwiftUI 的动画能力,包括:
- 属性动画:数值、颜色、布局等属性随状态变化平滑过渡
- 过渡动画:视图插入 / 移除时的进出效果(如淡入淡出、滑入滑出、翻转)
- 显式动画:通过
withAnimation 包裹一段「状态更新代码」统一加动画
Animation 类
Animation 用来描述「属性变化的时间曲线与节奏」,类似 SwiftUI 的 Animation。
工厂方法(创建动画)
Animation.default()
1static default(): Animation
- 创建一个默认动画(通常是系统预设的 ease-in-out 曲线)
- 无需配置,适合「只想要一个普通的过渡效果」的场景
示例:
1<Text animation={{
2 animation: Animation.default(),
3 value: value
4}}>默认动画</Text>
Animation.linear(duration?)
1static linear(duration?: DurationInSeconds | null): Animation
- 匀速动画,整段时间内速度保持恒定
duration:动画持续时间(秒),可选,不传时使用默认时长
适合:进度条数值增长、颜色线性变化等。
Animation.easeIn(duration?)
1static easeIn(duration?: DurationInSeconds | null): Animation
Animation.easeOut(duration?)
1static easeOut(duration?: DurationInSeconds | null): Animation
- 开始快、结尾慢
- 适合:元素「减速停止」的感觉,如卡片滑入后停在目标位置
Animation.bouncy(options?)
1static bouncy(options?: {
2 duration?: DurationInSeconds
3 extraBounce?: number
4}): Animation
-
带回弹效果的动画
-
参数:
duration:总时长(秒)
extraBounce:额外弹性,越大越明显
适合:按钮点击放大回弹、卡片弹出等「有趣」的动效。
Animation.smooth(options?)
1static smooth(options?: {
2 duration?: DurationInSeconds
3 extraBounce?: number
4}): Animation
- 相对柔和、过渡自然的动画
- 与
bouncy 相比,弹性感更弱,更偏「丝滑」
Animation.snappy(options?)
1static snappy(options?: {
2 duration?: DurationInSeconds
3 extraBounce?: number
4}): Animation
- 动作「干脆利落」,响应速度快
- 常见于触控反馈、选中高亮等瞬间反馈场景
Animation.spring(options?)
1static spring(options?: {
2 blendDuration?: number
3} & ({
4 duration?: DurationInSeconds
5 bounce?: number
6 response?: never
7 dampingFraction?: never
8} | {
9 response?: number
10 dampingFraction?: number
11 duration?: never
12 bounce?: never
13})): Animation
支持两种配置方式(注意互斥):
-
基于时长的弹簧动画
duration: 动画持续时间
bounce: 弹性大小
-
物理参数模式
response: 响应速度(值越小反馈越快)
dampingFraction: 阻尼系数(0~1,越大越「稳」,越小越「弹」)
额外参数:
blendDuration:动画混合时长,用于多动画衔接场景(可选)
示例:
1// 简单弹簧
2const anim1 = Animation.spring({
3 duration: 0.4,
4 bounce: 0.3
5})
6
7// 高级弹簧
8const anim2 = Animation.spring({
9 response: 0.25,
10 dampingFraction: 0.7
11})
Animation.interactiveSpring(options?)
1static interactiveSpring(options?: {
2 response?: number
3 dampingFraction?: number
4 blendDuration?: number
5}): Animation
- 面向「交互驱动」的弹簧动画,例如拖拽结束后的回弹
- 参数与
spring 的物理参数模式类似,语义更偏向手势交互
0 Animation.interpolatingSpring(options?)
1static interpolatingSpring(options?: {
2 mass?: number
3 stiffness: number
4 damping: number
5 initialVelocity?: number
6} | {
7 duration?: DurationInSeconds
8 bounce?: number
9 initialVelocity?: number
10 mass?: never
11 stiffness?: never
12 damping?: never
13}): Animation
两种配置方式(互斥):
-
物理参数模式
mass: 质量
stiffness: 刚度
damping: 阻尼
initialVelocity: 初速度(可选)
-
时长 + 弹性模式
duration: 动画时长
bounce: 弹性
initialVelocity: 初速度(可选)
适合对动态效果「非常在意手感」的高级场景。
修改已有动画(链式 API)
delay(time)
1delay(time: DurationInSeconds): Animation
- 使动画延迟
time 秒后再开始
- 返回一个新的
Animation 实例(原动画不变)
示例:
1const [animValue, setAnimValue] = useState(0)
2const anim = Animation
3 .spring({ duration: 0.4, bounce: 0.3 })
4 .delay(0.2)
5
6<Text animation={{
7 animation: anim,
8 value: animValue
9}>延迟弹簧</Text>
repeatCount(count, autoreverses?)
1repeatCount(count: number, autoreverses?: boolean): Animation
- 重复执行动画
count 次
autoreverses(默认 true):是否来回反向播放
示例:
1const pulse = Animation
2 .easeIn(0.6)
3 .repeatCount(3, true)
4
5<Text animation={{
6 animation: pulse,
7 value: value
8}}>闪烁三次</Text>
repeatForever(autoreverses?)
1repeatForever(autoreverses?: boolean): Animation
Animation 实战示例
示例 1:基本大小动画
1import { VStack, Button, Rectangle, useObservable, Animation, withAnimation } from "scripting"
2
3export function Demo() {
4 const size = useObservable(80)
5
6 return <VStack spacing={16}>
7 <Rectangle
8 frame={{ width: size.value, height: size.value }}
9 backgroundColor="blue"
10 animation={{
11 animation: Animation.spring({ duration: 0.3, bounce: 0.2 }),
12 value: size.value
13 }}
14 />
15
16 <Button
17 title="Toggle Size"
18 action={() => {
19 withAnimation(() => {
20 size.setValue(size.value === 80 ? 140 : 80)
21 })
22 }}
23 />
24 </VStack>
25}
Transition 类(视图过渡)
Transition 描述的是视图插入与移除时的「进场 / 退场效果」,对应 SwiftUI 的 AnyTransition。
注意:只有当视图在 JSX 中「存在与否」发生变化(如 {visible.value && <Text ... />})时,transition 才会生效。
实例方法
animation(animation?)
1animation(animation?: Animation): Transition
- 为当前过渡指定(或覆盖)使用的
Animation
- 不传时使用默认动画
示例:
1const t = Transition
2 .move("bottom")
3 .animation(Animation.spring({ duration: 0.4 }))
combined(other)
1combined(other: Transition): Transition
- 组合两个过渡效果,类似 SwiftUI 的
.combined
- 如:向下滑入 + 淡入
示例:
1const t = Transition
2 .move("bottom")
3 .combined(Transition.opacity())
在视图中使用:
1<Text transition={t}>组合过渡</Text>
静态方法(构造不同类型的过渡)
Transition.identity()
1static identity(): Transition
- 「没有任何过渡」,视图插入 / 移除时不会做动画
- 通常用于禁用某些分支的过渡效果
Transition.move(edge)
1static move(edge: Edge): Transition
- 从某个边缘移入 / 移出
edge 通常是 "leading" | "trailing" | "top" | "bottom" 等(和 SwiftUI 对齐)
示例:
1<Text transition={Transition.move("leading")}>
2 从左侧滑入 / 滑出
3</Text>
Transition.offset(position?)
1static offset(position?: Point): Transition
- 通过偏移实现过渡
position: { x: number, y: number },默认 { x: 0, y: 0 }
例如:
1<Text
2 transition={Transition.offset({ x: 0, y: 40 })}
3>
4 从下方位移进出
5</Text>
Transition.pushFrom(edge)
1static pushFrom(edge: Edge): Transition
- 类似导航 push 的效果,从某个边缘推入并把旧内容推走
- 适合做「页面切换」类效果
Transition.opacity()
1static opacity(): Transition
- 单纯的淡入 / 淡出
- 与
Animation 搭配可以控制淡入淡出的节奏
Transition.scale(scale?, anchor?)
1static scale(
2 scale?: number,
3 anchor?: Point | KeywordPoint
4): Transition
-
缩放过渡
-
scale:缩放比(默认 1)
-
anchor:缩放基准点,支持:
Point:如 { x: 0.5, y: 0.5 }
KeywordPoint:如 "center"、"top", "bottom" 等(具体值与 Scripting 内部对齐)
示例:
1<Text
2 transition={Transition.scale(0.8, "center")}
3>
4 缩放进出
5</Text>
Transition.slide()
1static slide(): Transition
- 类似 SwiftUI 的
.slide,通常是从一侧滑入 / 滑出(具体方向由系统决定)
- 常用于列表项、简单出现 / 消失效果
Transition.fade(duration?)
1static fade(duration?: DurationInSeconds): Transition
- 带时长配置的淡入 / 淡出
- 与
Transition.opacity() 类似,但可以直接指定过渡时间
Flip 系列(翻转过渡)
1static flipFromLeft(duration?: DurationInSeconds): Transition
2static flipFromBottom(duration?: DurationInSeconds): Transition
3static flipFromRight(duration?: DurationInSeconds): Transition
4static flipFromTop(duration?: DurationInSeconds): Transition
示例:
1<Text
2 transition={Transition.flipFromLeft(0.4)}
3>
4 左侧翻入 / 翻出
5</Text>
0 Transition.asymmetric(insertion, removal)
1static asymmetric(
2 insertion: Transition,
3 removal: Transition
4): Transition
- 插入和移除使用不同的过渡效果
- 典型用法:进入时从下方滑入,离开时淡出
示例:
1const appear = Transition
2 .move("bottom")
3 .combined(Transition.opacity())
4
5const disappear = Transition.opacity()
6
7const t = Transition.asymmetric(appear, disappear)
8
9<Text transition={t}>不对称过渡</Text>
Transition 实战示例
示例:多种过渡效果对比
1const visible = useObservable(true)
2
3return <VStack spacing={12}>
4 {visible.value &&
5 <Text
6 transition={Transition.slide().combined(Transition.opacity())}
7 >
8 Slide + Fade
9 </Text>
10 }
11
12 {visible.value &&
13 <Text
14 transition={Transition.move("leading")}
15 >
16 Move leading
17 </Text>
18 }
19
20 {visible.value &&
21 <Text
22 transition={Transition.scale()}
23 >
24 Scale
25 </Text>
26 }
27
28 <Button
29 title="Toggle"
30 action={() => {
31 withAnimation(() => {
32 visible.setValue(!visible.value)
33 })
34 }}
35 />
36</VStack>
withAnimation:显式动画入口
withAnimation 用来「显式」地将一段状态更新包裹在动画上下文中,类似 SwiftUI 的 withAnimation。
它返回 Promise<void>,方便在异步逻辑中等待动画完成。
重载签名
1function withAnimation(body: () => void): Promise<void>
2function withAnimation(animation: Animation, body: () => void): Promise<void>
3function withAnimation(
4 animation: Animation,
5 completionCriteria: "logicallyComplete" | "removed",
6 body: () => void
7): Promise<void>
-
第一个重载:使用默认动画
-
第二个重载:指定动画曲线 / 弹性等
-
第三个重载:额外指定完成条件:
"logicallyComplete":动画在时间轴上播放完成时视为完成(典型属性动画)
"removed":通常用于涉及过渡的场景,等待相关视图被移出 / 动画结束后再继续逻辑(具体行为依赖底层 SwiftUI)
实际等待的精确时机由内部动画系统决定,一般可理解为「该动画相关的视图不再处于动画中」。
基本用法
默认动画
1const size = useObservable(100)
2
3<Button
4 title="Toggle"
5 action={() => {
6 withAnimation(() => {
7 size.setValue(size.value === 100 ? 200 : 100)
8 })
9 }}
10/>
指定动画
1const visible = useObservable(true)
2
3<Button
4 title="Toggle Panel"
5 action={() => {
6 withAnimation(
7 Animation.spring({ duration: 0.3, bounce: 0.2 }),
8 () => {
9 visible.setValue(!visible.value)
10 }
11 )
12 }}
13/>
在异步函数中等待动画结束
1async function hideThenRunTask() {
2 await withAnimation(Animation.easeOut(0.25), () => {
3 visible.setValue(false)
4 })
5
6 // 此处可以认为相关动画已经结束,再继续耗时任务或导航
7 await doSomethingHeavy()
8}
视图上的 animation / transition 属性
在 Scripting 的视图组件上,可以通过 props 的形式配置动画相关行为:
animation?: Animation(属性动画)
transition?: Transition(插入 / 移除过渡)
属性动画(animation)
属性动画的核心逻辑:
- 当某个视图依赖的
Observable 的 value 发生变化时
- 如果该视图设置了
animation={...} 或更新发生在 withAnimation 中
- 则 SwiftUI 会对这些属性差异进行插值,从原值平滑过渡到新值
示例:
1const size = useObservable(80)
2
3<Rectangle
4 frame={{
5 width: size.value,
6 height:size.value
7 }}
8 backgroundColor="green"
9 animation={{
10 animation: Animation.spring({ duration: 0.3, bounce: 0.25 }),
11 value: size.value
12 }}
13/>
配合 withAnimation:
1<Button
2 title="Grow"
3 action={() => {
4 withAnimation(() => {
5 size.setValue(size.value + 20)
6 })
7 }}
8/>
过渡动画(transition)
过渡动画只在「视图从无到有 / 从有到无」时生效。
关键点:
示例:条件面板的进出过渡
1const visible = useObservable(false)
2
3<VStack>
4 {visible.value &&
5 <Text
6 transition={Transition
7 .move("bottom")
8 .combined(Transition.opacity())
9 .animation(Animation.spring({ duration: 0.35, bounce: 0.3 }))
10 }
11 >
12 Panel
13 </Text>
14 }
15
16 <Button
17 title="Toggle Panel"
18 action={() => {
19 withAnimation(() => {
20 visible.setValue(!visible.value)
21 })
22 }}
23 />
24</VStack>
综合示例:列表增删带过渡与属性动画
1import {
2 VStack,
3 HStack,
4 Text,
5 Button,
6 useObservable,
7 Animation,
8 Transition
9} from "scripting"
10
11type Item = { id: string; title: string }
12
13export function AnimatedList() {
14 const items = useObservable<Item[]>([
15 { id: "1", title: "First" },
16 { id: "2", title: "Second" }
17 ])
18
19 function addItem() {
20 withAnimation(Animation.spring({ duration: 0.3 }), () => {
21 const next = items.value.length + 1
22 items.setValue([
23 ...items.value,
24 { id: String(next), title: `Item ${next}` }
25 ])
26 })
27 }
28
29 function removeLast() {
30 if (items.value.length === 0) return
31 withAnimation(Animation.easeOut(0.25), () => {
32 items.setValue(items.value.slice(0, -1))
33 })
34 }
35
36 return <VStack spacing={12}>
37 {items.value.map(item =>
38 <HStack
39 key={item.id}
40 transition={Transition
41 .move("trailing")
42 .combined(Transition.opacity())
43 }
44 >
45 <Text>{item.title}</Text>
46 </HStack>
47 )}
48
49 <HStack spacing={12}>
50 <Button title="Add" action={addItem} />
51 <Button title="Remove Last" action={removeLast} />
52 </HStack>
53 </VStack>
54}
这个示例中:
- 使用
Observable<Item[]> 作为列表数据源
transition 负责列表项插入 / 删除时的滑动 + 淡入淡出
withAnimation 包裹增删操作,确保这些更新被动画化